/*
 * 代号：凤凰
 * http://www.jphenix.org
 * 2021-03-15
 * V4.0
 */
package com.jphenix.servlet.filter;

import java.util.List;
import java.util.Map;

import javax.servlet.FilterConfig;
import javax.servlet.http.HttpServletResponse;

import com.jphenix.driver.nodehandler.util.ParentChildNodeVO;
import com.jphenix.driver.regc.RegistCenterFilter;
import com.jphenix.driver.threadpool.ThreadSession;
import com.jphenix.service.nodeloader.NodeService;
import com.jphenix.servlet.common.ActionContext;
import com.jphenix.servlet.parent.ServiceBeanParent;
import com.jphenix.share.printstream.PrintWriterTool;
import com.jphenix.share.util.BaseUtil;
import com.jphenix.share.util.StringUtil;
import com.jphenix.standard.docs.BeanInfo;
import com.jphenix.standard.docs.ClassInfo;
import com.jphenix.standard.docs.Running;
import com.jphenix.standard.servlet.IActionBean;
import com.jphenix.standard.servlet.IActionContext;
import com.jphenix.standard.servlet.IFilter;
import com.jphenix.standard.servlet.IRequest;
import com.jphenix.standard.servlet.IResponse;
import com.jphenix.standard.viewhandler.INodeHandler;
import com.jphenix.standard.viewhandler.IViewHandler;

/**
 * 页面（模板）请求过滤器
 * com.jphenix.servlet.filter.PageFilter
 * 
 * 2021-04-27 解决了无法解析.ha扩展名的模板文件错误
 */
@Running({"90"})
@BeanInfo({"pagefilter"})
@ClassInfo({"2021-04-27 15:57","页面（模板）请求过滤器"})
public class PageFilter extends ServiceBeanParent implements IFilter {

  private String encoding          = null;  // 页面编码
  private NodeService ns           = null;  // 页面对象服务
  private RegistCenterFilter rcf   = null;  // 注册中心过滤器
  private String[] allowOriginVals = null;  // 允许跨站的域名数组，*为全部域名
  private String modelBasePath     = null;  // 模板根路径（相对网站根路径）
  private String modelExtName      = "htm"; // 模板扩展名
  /**
   * 构造函数
   */
  public PageFilter(){
    super();
  }

  /**
   * 覆盖方法
   * 刘虻
   * 2021年03月15日
   */
  @Override
  public int getIndex() {
    return 190;  //优先级最低，通常都是最后执行
  }

  /**
   * 覆盖方法
   * 刘虻
   * 2021年03月15日
   */
  @Override
  public String getFilterActionExtName() {
    return "htm,ha";
  }

	/**
	 * 执行初始化
	 * @param fe         过滤器管理类
	 * @param config     Servlet配置信息类
	 * @throws Exception 异常（如果初始化发生异常，则放弃不再使用）
	 * 2021年03月15日
	 * @author MBG
	 */
	@Override
	public void init(FilterExplorer fe, FilterConfig config) throws Exception {
    this.encoding = config.getInitParameter("encoding");
    if(this.encoding==null || this.encoding.length()<1) {
      this.encoding = "UTF-8";
    }
    modelBasePath = p("model_base_path");
    ns  = bean(NodeService.class);
    rcf = bean(RegistCenterFilter.class);
	}

  /**
	 * 覆盖方法
	 * 刘虻
	 * 2021-03-15 下午18:14:09
	 */
	@Override
  public boolean doFilter(IRequest req, IResponse resp) throws Exception {
    req.setCharacterEncoding(encoding);
    try {
      //强制输出编码格式为UTF-8
      resp.setCharacterEncoding("UTF-8");
    }catch(Error e) {}
    catch(Exception e) {}

    //动作路径
    String action = req.getServletPath();
    //去掉扩展名
    int point = action.lastIndexOf(".");
    if(point>-1) {
        action = action.substring(0,point);
    }
    //动作上下文
    IActionContext ac = newActionContext(action,req,resp);

    //放入动作路径和回话主键
    ThreadSession.put("_action_",action);                            //动作路径
    ThreadSession.put("_session_key_",ac.getSessionID()); //会话主键

    //清除缓存
    ac.getResponse().setHeader("Pragma","No-cache");
    ac.getResponse().setHeader("Cache-Control","no-cache");
    ac.getResponse().setDateHeader("Expires", 0);

    //解决跨域访问session丢失问题
    ac.getResponse().setHeader("P3P","CP=CAO PSA OUR"); 
    
    String[] vals = getAllowOriginVals(); //获取允许跨站域名数组
    for(int i=0;i<vals.length;i++) {
      ac.getResponse().setHeader("Access-Control-Allow-Origin",vals[i]); 
    }
    //设置页面类型，有些浏览器，如果不设置页面内容类型，则直接将代码输出到页面上
    ac.getResponse().setContentType("text/html");
    
    //构建视图文件路径
    String viewPath = modelBasePath+action+"."+modelExtName;
    //为反向动作调用
    //构建返回主界面
    INodeHandler nh = ns.getNodeHandlerByPath(ac,viewPath);
    if(nh==null || nh.isEmpty()){
      log.warning("Not Find The Model Of Action:["
        +action+"] BasePath:["+modelBasePath+"} Path:["+viewPath
        +"] Referer:["+ac.getRequest().getHeader("Referer")+"]",this);
      ac.getResponse().sendError(HttpServletResponse.SC_NOT_FOUND,ac.getRequest()); 
      return true;
    }
    //url中的请求参数字符串
    String queryStr = req.getQueryString();
    //不输出日志 （某些动作不输出日志，比如日志事件触发标记日志输出准备完毕的动作）
    if(queryStr.indexOf("_nolog_")>0) {
      outLog(false);
    }
    //之前遇到个无法呈现的问题，偶尔输出内容全是问号
    //原来使用的是vh.getDealEncode();这回写死UTF-8
    //基本输出编码 weblogic只能用基本编码输出
    String standardOutEncoding = "UTF-8";
    try {
        //设置编码格式
        ac.getResponse().setCharacterEncoding(standardOutEncoding);
    }catch(NoSuchMethodError e) {
        //使用WebLogic时会抛异常到这里，然后使用标准输出
        //非weblogic不能使用标准输出，否则反而出现乱码
        standardOutEncoding = "ISO-8859-1";
    }
    //获取动作对应的方法名
    Object time = log.runBefore(); //标记执行开始时间
    info("Start Execute PageAction:["+action+"]");
    try{
      parseNode(ac,nh); //执行解析模板
      //输出信息到页面
      nh.getNodeBody(
        new PrintWriterTool(
          ac.getResponse().getWriter(),
          nh.getDealEncode(),
          standardOutEncoding,
          false));
        //不能输出html代码到页面，太乱
    }catch(Exception e) {
      e.printStackTrace();
    }finally{
      log.writeRuntime(time,"Execute Complete Action:["+action+"]");
      outLog(true); //如果采用了线程池，需要手动重置这个值，否则就会出现有时无法输出日志的问题
    }
    return true;
  }

  /**
   * 处理 Access-Control-Allow-Origin
   * @return 允许跨站域名
   * 2019年6月28日
   * @author MBG
   */
  private String[] getAllowOriginVals() {
    if(allowOriginVals==null) {
      //获取允许跨站域名信息
      String val = p("servlet_access_control_allow_origin");
      if(val.length()<1) {
        allowOriginVals = new String[0];
      }else {
        allowOriginVals = BaseUtil.split(val,",",";","，","；");
      }
    }
    return allowOriginVals;
  }

  /**
   * 构造新的动作上下文
   * @param action 动作路径
   * @param req    请求对象
   * @param resp   返回对象
   * @return       动作上下文
   */
  private ActionContext newActionContext(String action,IRequest req,IResponse resp){
    return new ActionContext(req,resp,req.getServerName().toLowerCase(),action,uploadPath());
  }

  /**
   * 反向解析动作页面
   * 刘虻
   * 2010-6-8 下午05:02:15
   * @param actionContext 动作上下文
   * @param nh 视图对象
   * @throws Exception 执行发生异常
   */
  public void parseNode(String action,IRequest req,IResponse resp,IViewHandler nh) throws Exception {
    parseNode(newActionContext(action,req,resp),nh);
  }

  /**
   * 反向解析动作页面
   * 刘虻
   * 2010-6-8 下午05:02:15
   * @param actionContext 动作上下文
   * @param nh 视图对象
   * @throws Exception 执行发生异常
   */
  public void parseNode(IActionContext actionContext,IViewHandler nh) throws Exception {
      Object time = log.runBefore(); //标记执行开始时间
      //获取执行页面的主子信息类
      ParentChildNodeVO pcn = ParentChildNodeVO.fixParentChildNodeVOByAttribute(nh,"_action");
      parseNode(actionContext,pcn); //准备递归调用解析模板
      log.writeRuntime(time,"Parse Html Use Time");
  }

  /**
   * 反向解析动作页面 (递归函数)
   * 刘虻
   * 2010-6-8 下午05:02:15
   * @param actionContext 动作上下文
   * @param nh 视图对象
   * @return  解析完当前段后，还要不要往下解析
   * 通常都会继续往下解析，除非遇到了执行某个块动作时，
   * 重新加载了整个页面，也就没有必要继续解析以前旧的内容了
   * @throws Exception 执行发生异常
   */
  protected boolean parseNode(
          IActionContext actionContext,ParentChildNodeVO pcn) throws Exception {
    if(pcn==null) {
        return true;
    }
    if(pcn.hasChild()) {
      List<ParentChildNodeVO> cNodeList = pcn.getChildList(); //获取子节点
      for (int i=0;i<cNodeList.size();i++) {
        if(!parseNode(actionContext, cNodeList.get(i))) {
          return false;
        }
      }
    }
    //获取子标签元素
    IViewHandler cNh = pcn.getView();
    //移出标签
    cNh.removeAttribute("_execute");
    //是否隐藏声明标签
    if(boo(cNh.getAttribute("_hiddenself")) 
            || boo(cNh.getAttribute("_hidden")) ) {
        cNh.setOutSelf(false);
    }
    return fixPageAction(actionContext,pcn);
  }

  /**
   * 处理动作节点
   * 刘虻
   * 2010-6-9 下午06:42:49
   * @param ac 动作上下文
   * @param pcn 节点模块
   * @return 执行完当前动作后，能否继续执行下一个动作
   * 通常都会继续往下解析，除非遇到了执行某个块动作时，
   * 重新加载了整个页面，也就没有必要继续解析以前旧的内容了
   * @throws Exception 执行发生异常
   */
  protected boolean fixPageAction(
          IActionContext ac
          ,ParentChildNodeVO pcn) throws Exception {
    IViewHandler cVh = pcn.getView(); //获取待处理的子节点
    //获取动作路径
    String action = cVh.ra("_action");
    if(action.length()<1) {
      return true;
    }
    log("Parse Model Page Execute Action:["+action+"]");
    int point = action.indexOf("?"); //参数分隔符
    if(point>0) {
      //模板中调用该动作传入的固定参数，该参数优先顺序排在url传入参数之后
      Map<String,String> modelParaMap = StringUtil.fixQueryString(action.substring(point+1));
      action = action.substring(0,point);
      List<String> keyList = BaseUtil.getMapKeyList(modelParaMap); //参数主键序列
      for(String key:keyList) {
        if(!ac.hasParameter(key)) {
          ac.setParameter(key,new String[] {str(modelParaMap.get(key))});
        }
      }
    }
    //脚本主键
    String scriptId = action.toUpperCase();
    if(!action.startsWith("/")) {
        action = "/"+action;
    }
    point = scriptId.lastIndexOf("/"); //分隔符
    if(point>-1) {
      scriptId = scriptId.substring(point+1);
    }
    //检查相关节点
    String relateIds = cVh.a("_relateids");
    if(relateIds.length()>0) {
      //分割成序列
      List<String> relateIdList = BaseUtil.splitToList(relateIds,",");
      IViewHandler tVh          = ns.getNewViewHandler(); //构建抽象类
      tVh.setOutSelf(false);
      tVh.addChildNode(cVh);
      //获取当前节点的根节点
      IViewHandler baseVh = cVh.getBaseNode();
      for(String relateId:relateIdList) {
        if(relateId==null || relateId.length()<1) {
          continue;
        }
        //获取对应的相关节点
        IViewHandler vh = baseVh.getFirstChildNodeByID(relateId);
        if(vh.isEmpty()) {
          continue;
        }
        tVh.addChildNode(vh);
      }
      cVh = tVh;
    }
    //动作类
    IActionBean cab = rcf.getActionScript(action,scriptId,ac,null);
    if(cab==null) {
      return false;
    }
    //设置传入参数
    cab.setModelParameterMap(
            StringUtil
                .getParaMapFromString(
                        cVh.getAttribute("_para")));
    cVh.removeAttribute("_para");
    //获取是否不使用内置html
    boolean noInnerHtml = boo(cVh.getAttribute("_nohtml"));
    cVh.removeAttribute("_nohtml");
    if (!noInnerHtml) {
        //设置操作主界面
        cab.setReVh(cVh);
    }
    //执行脚本动作
    rcf.executeAction(action,cab,ac,null);
    return !cab.isOver();
  }
}
